package pl.polsl.mmm.projectcomm.objects.MD5;

import java.util.ArrayList;

import pl.polsl.mmm.projectcomm.data.Color;
import pl.polsl.mmm.projectcomm.data.vec3;
import pl.polsl.mmm.projectcomm.data.vec4;
import pl.polsl.mmm.projectcomm.objects.Lines;
import pl.polsl.mmm.projectcomm.programs.ColorShaderProgram;
import pl.polsl.mmm.projectcomm.programs.TextureLightShaderProgram;
import pl.polsl.mmm.projectcomm.programs.TextureShaderProgram;

import static pl.polsl.mmm.projectcomm.Constants.*;

/**
 * Klasa opisująca całość modelu MD5. Składają się na niego: meshe, akcje(animacje),
 * pozycja początkowa szkieletu. Zawiera również wektor położenia, skali i rotacji całego obiektu.
 * Created by Maciek on 2014-11-01.
 */
public class MD5 {
    //tablica z meshami składającymi się na 1 model
    public MD5Mesh	md5mesh[];
    //tablica z tzw. bind pose, czyli pozycją wyjściową modelu
    public MD5Joint bindPose[];
    //lista akcji (np. skok, ruch, kucnięcie itp )
    public ArrayList<MD5Action> md5action;
    //nazwa modelu
    private String	name;
    //ilość kości
    private int nJoint;
    //ilość meshy składających się na cały model
    private int nMesh;
    //ilość akcji/animacji
    private int	nAction;
    //flaga czy model jest wyświetlany
    private boolean	visible;
    //obiekt do wyświetlania linii szkieletu
    private Lines skeletonLines;


    public MD5() {
        md5action = new ArrayList<MD5Action>();
    }
    /**Metoda do alokowania tablicy z pozycją wyjściową.
     * Uwaga: wypełnia od razu pole nJoint!!*/
    public void alocateBindPose (int nJoint) {
        this.nJoint = nJoint;
        bindPose = new MD5Joint[nJoint];
    }

    /**Metoda do alokowania tablicy z meshami.
     * Uwaga: wypełnia od razu pole nMesh*/
    public void alocateMesh(int nMesh) {
        this.nMesh = nMesh;
        md5mesh = new MD5Mesh[this.nMesh];
        for(int i=0;i<nMesh;i++)
            md5mesh[i] = new MD5Mesh();
    }

    /** Inkrementacja aktualnie wpisanych/wczytanych akcji */
    public void incnAction() {
        nAction++;
    }

    /** Funkcja zwalniajaca dane o modelu MD5*/
    public void freeMD5 () {
        for(int i=0; i<getnMesh() ;i++) {
            md5mesh[i].cleanUp();
        }
        md5mesh = null;
        for(MD5Action a : md5action ) {
            a.clearUp();
        }
        md5action.clear();
        bindPose = null;
    }

    /** Metoda do pobrania akcji z listy o określonej nazwie */
    public MD5Action getMD5ActionByName(String name) {
        for(MD5Action act : md5action) {
            if(name.compareTo(act.getName())==0) {
                return act;
            }
        }
        return null;
    }
    /** Sklejanie kilku animacji w jedną */
    public void blendPose(MD5Joint[] finalPose, MD5Joint[] pose0, MD5Joint[] pose1,
                          float blend) {
        for(int i=0;i<nJoint;i++) {
            finalPose[i].getLocation().lerp(
                    pose0[i].getLocation(),
                    pose1[i].getLocation(),
                    blend);

            finalPose[i].getRotation().slerp(
                    pose0[i].getRotation(),
                    pose1[i].getRotation(),
                    blend

            );
        }
    }
    /** Jeszcze dokładnie nie wiem po co to, ale domyślam się, że dodaje to klatkę animacji */
    public void addPose(MD5Joint[] finalPose, MD5Action action0, MD5Action action1,
                        float actionWeight) {
        for(int i=0;i<nJoint;i++) {
            //sprawdzenie czy wektory lokacji i rotacji są identyczne dla obecnej i następnej ramki
            //jeśli NIE...
            if( !vec3.vec3Compare(action1.getFrame()[action1.getCurr_frame()][i].getLocation(),
                    action1.getFrame()[action1.getNext_frame()][i].getLocation())
                    || !vec4.vec4Compare(action1.getFrame()[action1.getCurr_frame()][i].getRotation(),
                    action1.getFrame()[action1.getNext_frame()][i].getRotation())){
                //interpolacja wektorów lokacji
                finalPose[i].getLocation().lerp(
                        action0.getPose()[i].getLocation(),
                        action1.getPose()[i].getLocation(),
                        actionWeight);


                finalPose[i].getRotation().slerp(
                        action0.getPose()[i].getRotation(),
                        action1.getPose()[i].getRotation(),
                        actionWeight);

                //jeśli TAK...
            } else {
                //przepisujemy pozę z akcji 0 bo i tak sprawdziliśmy że jest taka sama jak w akcji 1
                finalPose[i].setLocation(action0.getPose()[i].getLocation());
                finalPose[i].setRotation(action0.getPose()[i].getRotation());
            }
        }
    }

    /** Przebudowanie modelu ( stworzenie VBO i wypełnienie, wyliczenie normali i tangentów ) */
    public void firstBuild(int[] textures) {
        for(int i=0;i<nMesh;i++) {
            md5mesh[i].prepareMesh(bindPose);
            md5mesh[i].prepareNormals(bindPose);
            md5mesh[i].buildVbo();
            //włożenie do buforów danych wierzchołków pos/normal/uv
            md5mesh[i].insertVertexData();
            String texName = md5mesh[i].getTextureName();
            if(texName.compareTo("body")==0)
                md5mesh[i].setTexture(textures[0]);
            else if(texName.compareTo("head")==0)
                md5mesh[i].setTexture(textures[1]);
            else if(texName.compareTo("helmet")==0)
                md5mesh[i].setTexture(textures[2]);
            else if(texName.compareTo("lamp")==0)
                md5mesh[i].setTexture(textures[3]);
        }
    }

    /** Metoda do aktualizowania obiektów typu MD5Vertex. Można korzystać w pętli drawLines*/
    public void actualizeBuild(MD5Joint[] pose) {
        for(int i=0;i<nMesh;i++) {
            md5mesh[i].prepareMesh(pose);
            md5mesh[i].prepareNormals(pose);
            md5mesh[i].insertVertexData();
        }

    }
    /** Rysowanie akcji */
    public boolean drawAction(float timeStep) {
        boolean update = false;
        for(MD5Action action : md5action) {
            if(action.getState() == ACTION_STATE.PLAY) {
                action.setFrameTime(action.getFrameTime() + timeStep);
                switch (action.getMethod()) {
                    case MD5_METHOD_FRAME: {
                        if(action.getFrameTime() >= action.getFps()) {
                            action.setPose(action.getFrame()[action.getCurr_frame()]);
                            action.incCurr_frame();
                            if(action.getCurr_frame() == action.getnFrame()) {
                                if(action.isLoop()) action.setCurr_frame(0);
                                else {
                                    action.actionStop();
                                    break;
                                }
                            }
                            action.setNext_frame(action.getCurr_frame() + 1);
                            if(action.getNext_frame() == action.getnFrame()) action.setNext_frame(0);

                            action.setFrameTime(action.getFrameTime() - action.getFps());
                            update = true;
                        }
                        break;
                    }
                    case MD5_METHOD_LERP:
                    case MD5_METHOD_SLERP: {
                        //clamp
                        float t = action.getFrameTime() / action.getFps();
                        if(t > 1f) t = 1f;
                        else if (t<0f) t = 0f;
                        blendPose(
                                action.getPose(),
                                action.getFrame()[action.getCurr_frame()],
                                action.getFrame()[action.getNext_frame()],
                                t);
                        if(t >= 1f) {
                            action.incCurr_frame();
                            action.setNext_frame(action.getCurr_frame() + 1 );
                            if( action.isLoop()) {
                                if(action.getCurr_frame() == action.getnFrame()) {
                                    action.setCurr_frame(0);
                                    action.setNext_frame(1);
                                }

                                if(action.getNext_frame() == action.getnFrame())
                                    action.setNext_frame(0);
                            } else {
                                if(action.getNext_frame() == action.getnFrame()) {
                                    action.actionStop();
                                    break;
                                }
                            }
                            action.setFrameTime(action.getFrameTime() - action.getFps());
                        }
                        update = true;
                        break;
                    }
                }
            }
        }
        return update;
    }
    /** Rysowanie mesha */
    public void draw(TextureLightShaderProgram textureProgram) {
        if(visible) {
            for(int i=0;i<nMesh;i++) {
                if(md5mesh[i].isVisible()) {
                    //ustawienie tekstury dla aktualnie rysowanego mesha
                    textureProgram.setTexture(md5mesh[i].getTexture());
                    //ustawienie paremtrów atrybutów shadera i rysowanie mesha
                    md5mesh[i].drawSetAttribPointer(textureProgram);
                }
            }
        }
    }
    /** Metoda do stworzenia tablicy linii kości (do debugu) */
    public void alocateSkeletonLines() {
        skeletonLines = new Lines(nJoint);
    }
    /** Metoda do rysowania pozycji kości (do debugu!!) */
    public void drawSkeletonLines(MD5Joint[] pose, ColorShaderProgram colorProgram) {
        int parent;
        for(int i=0;i<nJoint;i++) {
            parent = pose[i].getParent();
            if(parent == -1) parent = 0;
            skeletonLines.putBonesData(
                    pose[i].getLocation(),
                    pose[parent].getLocation()
            );
        }
        //ustawienie koloru linii
        Color linesColor = new Color(0f,1f,0f,1f);
        colorProgram.setColor(linesColor.getR(),linesColor.getG(),linesColor.getB());
        skeletonLines.bindData(colorProgram);
        skeletonLines.drawLines();
        //ustawienie koloru punktów
        linesColor = new Color(0f,0f,1f,1f);
        colorProgram.setColor(linesColor.getR(),linesColor.getG(),linesColor.getB());
        skeletonLines.drawPoints();
    }
    /** metoda do inicjalizacji obiektów rysujących linie normali w debugu */
    public void alocateDebugNormalsLines() {
        for(int i=0;i<nMesh;i++) {
            md5mesh[i].alocateNormalLines();
        }
    }
    /** Rysowanie linii normali na meshach. TYLKO DO DEBUGU */
    public void drawNormalLines(ColorShaderProgram colorProgram) {
        //ustawienie koloru linii
        Color linesColor = new Color(1f,1f,0f,1f);
        colorProgram.setColor(linesColor.getR(),linesColor.getG(),linesColor.getB());
        for(int i=0;i<nMesh;i++) {
            md5mesh[i].drawDebugNormals(colorProgram);
        }
    }
    /***********************SETTERY I GETTERY*****************************************/
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    public int getnJoint() {
        return nJoint;
    }

    public void setnJoint(int nJoint) {
        this.nJoint = nJoint;
    }

    public int getnMesh() {
        return nMesh;
    }

    public void setnMesh(int nMesh) {
        this.nMesh = nMesh;
    }

    public int getnAction() {
        return nAction;
    }

    public void setnAction(int nAction) {
        this.nAction = nAction;
    }

    public void setMd5action(ArrayList<MD5Action> md5action) {
        this.md5action = md5action;
    }

}
